from typing import Tuple, overload, Self, List
from abc import ABCMeta, abstractmethod
import pygame

from game.container import Variables, Direction
from game.world.coordinate import Coordinate, CoordinateType
from game.data.plot.plot import Dialog


class Frames:
    
    def __init__(
        self,
        frame_N: Tuple[pygame.Surface, ...],
        frame_NE: Tuple[pygame.Surface, ...],
        frame_E: Tuple[pygame.Surface, ...],
        frame_SE: Tuple[pygame.Surface, ...],
        frame_S: Tuple[pygame.Surface, ...],
        frame_SW: Tuple[pygame.Surface, ...],
        frame_W: Tuple[pygame.Surface, ...],
        frame_NW: Tuple[pygame.Surface, ...],
        direction = Direction.S
    ):
        self.frame_N = frame_N
        self.frame_NE = frame_NE
        self.frame_E = frame_E
        self.frame_SE = frame_SE
        self.frame_S = frame_S
        self.frame_SW = frame_SW
        self.frame_W = frame_W
        self.frame_NW = frame_NW
        self.direction = direction

        self.frame = 0

    def _match_direction(self) -> Tuple[pygame.Surface, ...]:
        match self.direction:
            case Direction.N: return self.frame_N
            case Direction.NE: return self.frame_NE
            case Direction.E: return self.frame_E
            case Direction.SE: return self.frame_SE
            case Direction.S: return self.frame_S
            case Direction.SW: return self.frame_SW
            case Direction.W: return self.frame_W
            case Direction.NW: return self.frame_NW
            
    def change_direction(self, direction: Direction) -> None:
        if direction != Direction.NONE:
            self.direction = direction
        self.reset_frame()
        
    def reset_frame(self) -> None:
        self.frame = 0
            
    def get_frame(self) -> pygame.Surface:
        return self._match_direction()[self.frame]
    
    def next_frame(self) -> pygame.Surface:
        self.frame = (self.frame + 1) % len(self._match_direction())
        return self.get_frame()

class AnimatedSprite(pygame.sprite.Sprite, metaclass=ABCMeta):
    
    def __init__(
        self,
        
        coordinate: Coordinate,
        groups: Tuple[pygame.sprite.Group, ...],
        
        frames: Frames
    ):
        super().__init__(*groups)
        
        self.display_surface = Variables.screen
        
        self.frames = frames
        
        self.surface = self.frames.get_frame()
        self.rect = self.surface.get_rect(center=coordinate.Vector2)
        
    def animate(self) -> None:
        self.surface = self.frames.next_frame()
        self.rect = self.surface.get_rect(center=self.rect.center)
        
    def update(self) -> None:
        # self.display_surface.blit(self.surface, self.rect)
        pass
        
        

class Entity(AnimatedSprite, metaclass=ABCMeta):
    
    def __init__(
        self,
        
        coordinate: Coordinate,
        
        groups: Tuple[pygame.sprite.Group, ...],
        
        frames: Frames,

        dialog: Dialog | None = None,
        can_interact: bool = False,
        can_communicate: bool = False
    ):
        super().__init__(coordinate, groups, frames)
        
        self.direction: Direction = Direction.NONE
        self.coordinate = coordinate
        self.dest: Coordinate | None = None
        
        self.speed = 3
        self.mask = pygame.mask.from_surface(self.surface)
        self.dialog = dialog
        self.can_interact = can_interact
        self.can_communicate = can_communicate
        
        self.is_barrier = False
        

    @abstractmethod
    def moveTo(self, dest: Coordinate) -> bool:
        """ Move to dest, return true if reached """
    
    @overload
    def getDistance(self, __entity: Self) -> float: ...
    @overload
    def getDistance(self, __coordinate: CoordinateType) -> float: ...
    @overload
    def getDistance(self, __coordinate: Coordinate) -> float: ...
    def getDistance(self, x: Self | CoordinateType | Coordinate) -> float:
        if isinstance(x, self.__class__):
            return self.coordinate.getDistance(x.coordinate)
        return self.coordinate.getDistance(x) # type: ignore
    
    @overload
    def setDestination(self, dest: Coordinate) -> None: ...
    @overload
    def setDestination(self, dest: CoordinateType) -> None: ...
    def setDestination(self, dest: Coordinate | CoordinateType) -> None:
        self.dest = Coordinate(dest)
    
    def update(self) -> None:
        if self.dest and self.moveTo(self.dest):
            self.dest = None
        
        super().update()
